[do not merge] feat: Span streaming & new span API#5551
[do not merge] feat: Span streaming & new span API#5551sentrivana wants to merge 170 commits intomasterfrom
Conversation
There was a problem hiding this comment.
SpanBatcher lacks unit tests for new size-based flush logic (sentry_sdk/_span_batcher.py:15)
The new SpanBatcher class introduces size-based flush limits (MAX_BYTES_BEFORE_FLUSH, _running_size, _estimate_size) and batching logic, but there are no unit tests for this component. Key untested scenarios include: size-based flush triggering, the envelope splitting logic when exceeding MAX_ENVELOPE_SIZE (1000 spans), and the queue overflow handling.
_finished check placed too late allows double-counting of lost events (sentry_sdk/traces.py:441)
The _finished check at line 441 occurs after the lost event recording for discarded spans (lines 425-438). If _end() is called multiple times on the same span (e.g., explicit end() call followed by context manager __exit__), the span will record lost events to the transport before detecting it's already finished. This causes incorrect telemetry data.
Identified by Warden code-review
| if span is not None: | ||
| span.set_status(SPANSTATUS.INTERNAL_ERROR) | ||
| if isinstance(span, StreamedSpan): | ||
| span.set_status(SpanStatus.ERROR) |
There was a problem hiding this comment.
StreamedSpan lacks set_status() method causing AttributeError
The code calls span.set_status(SpanStatus.ERROR) on a StreamedSpan instance, but StreamedSpan does not have a set_status() method - it only has a status property setter. This will raise an AttributeError at runtime when a SQLAlchemy query errors in streaming mode. Other integrations (e.g., Celery at line 105) correctly use span.status = SpanStatus.ERROR instead.
Verification
Read sentry_sdk/traces.py to confirm StreamedSpan class definition (lines 188-399). The class has status property getter/setter (lines 386-398) but no set_status() method. Verified correct usage pattern in sentry_sdk/tracing_utils.py line 1125 and sentry_sdk/integrations/celery/init.py line 105 which both use property assignment span.status = SpanStatus.ERROR.
Suggested fix: Use the status property setter instead of calling the non-existent set_status method
| span.set_status(SpanStatus.ERROR) | |
| span.status = SpanStatus.ERROR |
Identified by Warden code-review · NUR-VFN
| if not client.is_active(): | ||
| return |
There was a problem hiding this comment.
_end() returns early without setting _finished flag when client is inactive
When client.is_active() returns False at line 326, the method returns early without setting self._finished = True. However, the scope detachment (lines 317-321) and profiler stop (line 314) have already occurred. If _end() is called again later when the client becomes active, the method will: (1) try to access the deleted _context_manager_state attribute causing a silently-swallowed AttributeError, and (2) attempt to stop an already-stopped profiler. While capture_internal_exceptions() prevents crashes, this leads to inconsistent state and harder-to-debug issues.
Verification
Read sentry_sdk/traces.py lines 306-342 to trace the _end() method flow. Confirmed that _finished is only set on line 341 which is after the early return on line 327. Verified that scope detachment (line 320: del self._context_manager_state) and profiler stop (line 314) occur before the client.is_active() check.
Suggested fix: Move the self._finished = True assignment to occur before the early return, or restructure the method to handle the inactive client case properly.
| if not client.is_active(): | |
| return | |
| self._finished = True | |
Identified by Warden code-review · 26A-B4F
| def __init__( | ||
| self, | ||
| unsampled_reason: "Optional[str]" = None, | ||
| scope: "Optional[sentry_sdk.Scope]" = None, | ||
| **kwargs: "Any", | ||
| ) -> None: | ||
| self._segment = None # type: ignore[assignment] | ||
| self._scope = scope # type: ignore[assignment] | ||
| self._unsampled_reason = unsampled_reason | ||
|
|
||
| self._start() |
There was a problem hiding this comment.
NoOpStreamedSpan missing attribute initialization causes AttributeError
NoOpStreamedSpan inherits from StreamedSpan but its __init__ doesn't initialize many attributes that inherited methods/properties access. Properties like active, timestamp, start_timestamp, status, and methods like get_baggage(), get_trace_context(), dynamic_sampling_context() will raise AttributeError when called because attributes like _active, _timestamp, _start_timestamp, _status, _baggage, _parent_span_id are never set. Any code path using these NoOpStreamedSpan instances that accesses these inherited members will fail at runtime.
Verification
Read the full traces.py file. Traced StreamedSpan.init (lines 221-275) which initializes _active, _timestamp, _start_timestamp, _status, _baggage, _parent_span_id etc. NoOpStreamedSpan.init (lines 539-549) only sets _segment, _scope, _unsampled_reason. Checked slots on both classes. Verified inherited properties/methods like active (line 408), timestamp (line 430), start_timestamp (line 434), status (line 386), get_baggage (line 490), get_trace_context (line 503) access these uninitialized attributes.
Identified by Warden code-review · TSD-SSZ
Introduce a new
start_span()API with a simpler and more intuitive signature to eventually replace the originalstart_span()andstart_transaction()APIs.Additionally, introduce a new streaming mode (
sentry_sdk.init(_experiments={"trace_lifecycle": "stream"})) that will send spans as they finish, rather than by transaction.The new API MUST be used with the new streaming mode, and the old API MUST be used in the legacy non-streaming (static) mode.
Migration guide: getsentry/sentry-docs#16072
Notes
Spanand drop the newStreamedSpanintracing.pyas a replacement.trace_id(we can't send spans from different traces in the same envelope).Release Plan
Project
https://linear.app/getsentry/project/span-first-sdk-python-727da28dd037/overview